// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: donggu

#include "coin2/exchange/base/api_util/json_schema.h"

#include <experimental/filesystem>
#include <string_view>

#include <fmt/format.h>
#include <rapidjson/document.h>
#include <rapidjson/encodings.h>
#include <rapidjson/error/en.h>
#include <rapidjson/filereadstream.h>
#include <rapidjson/schema.h>
#include <rapidjson/stringbuffer.h>

#include "coin2/base/log.h"

namespace {
// https://www.jsonschemavalidator.net/
rapidjson::Document LoadSchemaDocument(const std::experimental::filesystem::path& path) {
  std::unique_ptr<char[]> buf(new char[32768]);

  FILE* fp = fopen(path.c_str(), "r");
  if (!fp) {
    throw std::runtime_error(fmt::format("Cannot read JsonSchema: {}", path.c_str()));
  }
  rapidjson::FileReadStream is(fp, buf.get(), 32768);
  rapidjson::Document d;
  d.ParseStream(is);
  fclose(fp);

  if (d.HasParseError()) {
    throw std::runtime_error(
        fmt::format("File exists, but Cannot parse JsonSchema: {}", path.c_str()));
  }

  return d;
}

}  // namespace

class JsonSchemaImpl {
 public:
  explicit JsonSchemaImpl(const std::experimental::filesystem::path& path)
      : schema_(LoadSchemaDocument(path)), validator_(schema_) {}

  bool IsAccept(const rapidjson::Document& doc) {
    validator_.Reset();
    return doc.Accept(validator_);
  }

  void AcceptOrThrow(const rapidjson::Document& doc) {
    validator_.Reset();
    bool is_accept = doc.Accept(validator_);
    if (!is_accept) {
      rapidjson::StringBuffer sb;
      validator_.GetInvalidSchemaPointer().StringifyUriFragment(sb);
      LOG(ERROR) << "Invalid schema: " << sb.GetString();
      LOG(ERROR) << "Invalid keyword: " << validator_.GetInvalidSchemaKeyword();
      sb.Clear();
      validator_.GetInvalidDocumentPointer().StringifyUriFragment(sb);
      LOG(ERROR) << "Invalid document: " << sb.GetString();
      throw std::runtime_error("json schema didn't accept the doc");
    }
  }

 private:
  rapidjson::SchemaDocument schema_;
  rapidjson::SchemaValidator validator_;
};

JsonSchema::JsonSchema(const std::experimental::filesystem::path& path)
    : impl_(new JsonSchemaImpl(path)) {}
JsonSchema::~JsonSchema() = default;

bool JsonSchema::IsAccept(std::string_view s) {
  rapidjson::Document d;
  d.Parse(s.data());
  if (d.HasParseError()) {
    return false;
  }
  return IsAccept(d);
}
bool JsonSchema::IsAccept(const rapidjson::Document& d) { return impl_->IsAccept(d); }
void JsonSchema::AcceptOrThrow(std::string_view s) {
  rapidjson::Document d;
  d.Parse(s.data());
  if (d.HasParseError()) {
    throw std::runtime_error(
        fmt::format("JsonSchema throws: {}", GetParseError_En(d.GetParseError())));
  }
  AcceptOrThrow(d);
}
void JsonSchema::AcceptOrThrow(const rapidjson::Document& d) { return impl_->AcceptOrThrow(d); }
